// Copyright 2011 Edward Wahlen. All Rights Reserved.

package org.wahlen.io;

import com.google.common.base.Preconditions;
import com.google.common.collect.AbstractIterator;

import org.wahlen.pc.Marshaller;
import org.wahlen.util.Pair;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.Iterator;

/**
 * @author edward.wahlen@gmail (Edward Wahlen)
 */
public class Marshallers {

  public static final Marshaller<Void> VOID_MARSHALLER = new Marshaller<Void>() {
    @Override
    public Void read(DataInputStream dis) throws IOException {
      return null;
    }

    @Override
    public void write(Void item, DataOutputStream dos) throws IOException {
    }
  };

  public static Marshaller<Integer> unboxedInt() {
    return new Marshaller<Integer>() {
      public Integer read(DataInputStream dis) throws IOException {
        return dis.readInt();
      }

      public void write(Integer item, DataOutputStream dos) throws IOException {
        dos.writeInt(item);
      }
    };
  }

  public static VarIntMarshaller unboxedVarInt() {
    return new VarIntMarshaller();
  }

  public static <F, S> Marshaller<Pair<F, S>> getPair(final Marshaller<F> f,
      final Marshaller<S> s) {
    return new Marshaller<Pair<F, S>>() {
      public Pair<F, S> read(DataInputStream dis) throws IOException {
        return Pair.of(f.read(dis), s.read(dis));
      }

      public void write(Pair<F, S> item, DataOutputStream dos) throws IOException {
        f.write(item.first, dos);
        s.write(item.second, dos);
      }
    };
  }

  /**
   * Returns a Marshaller for an Iterable by encoding a byte of 1 before every item and terminating
   * with a zero byte. The Iterable is "oneShot" because the input stream is not buffered and the
   * iterator cannot be fetched repeatedly.
   */
  public static <T> Marshaller<Iterable<T>> oneShotIterable(final Marshaller<T> m) {
    return new Marshaller<Iterable<T>>() {
      public Iterable<T> read(final DataInputStream dis) throws IOException {
        return oneShot(new Iterable<T>() {
          public Iterator<T> iterator() {
            return new AbstractIterator<T>() {
              @Override
              protected T computeNext() {
                try {
                  return dis.readByte() != 0 ? m.read(dis) : endOfData();
                } catch (IOException e) {
                  throw new AssertionError(e);
                }
              }
            };
          }
        });
      }

      public void write(Iterable<T> item, DataOutputStream dos) throws IOException {
        for (T t : item) {
          dos.write(1);
          m.write(t, dos);
        }
        dos.write(0);
      }
    };
  }

  static <T> Iterable<T> oneShot(final Iterable<T> items) {
    return new Iterable<T>() {
      private boolean iteratorNotYetCalled = true;

      public Iterator<T> iterator() {
        Preconditions.checkState(iteratorNotYetCalled);
        iteratorNotYetCalled = false;
        return items.iterator();
      }
    };
  }

  public static class VarIntMarshaller implements Marshaller<Integer> {

    public Integer read(DataInputStream dis) throws IOException {
      return readUnboxed(dis);
    }

    public void write(Integer item, DataOutputStream dos) throws IOException {
      writeUnboxed(item, dos);
    }

    public int readUnboxed(DataInputStream dis) throws IOException {
      int b = dis.read();
      if (b < 0) {
        throw new EOFException();
      }
      if ((b & 128) == 0) {
        return b;
      }
      int result = (b & 127) << 7;
      b = dis.read();
      if (b < 0) {
        throw new EOFException();
      }
      if ((b & 128) == 0) {
        return result + b;
      }
      result = (result + (b & 127)) << 7;
      b = dis.read();
      if (b < 0) {
        throw new EOFException();
      }
      if ((b & 128) == 0) {
        return result + b;
      }
      result = (result + (b & 127)) << 7;
      b = dis.read();
      if (b < 0) {
        throw new EOFException();
      }
      if ((b & 128) == 0) {
        return result + b;
      }
      result = (result + (b & 127)) << 7;
      b = dis.read();
      if (b < 0) {
        throw new EOFException();
      }
      return result + b;
    }

    public void writeUnboxed(int item, DataOutputStream dos) throws IOException {
      if (item >= 268435456) {
        dos.write(128 | (item >> 28));
      }
      if (item >= 2097152) {
        dos.write(128 | (item >> 21));
      }
      if (item >= (16384)) {
        dos.write(128 | (item >> 14));
      }
      if (item >= 128) {
        dos.write(128 | (item >> 7));
      }
      if (item >= 0) {
        dos.write(item & 127);
        return;
      }
      // item < 0
      dos.write(item >> 28);
      dos.write((item >> 21) | 128);
      dos.write((item >> 14) | 128);
      dos.write((item >> 7) | 128);
      dos.write(item & 127);
    }
  }
}
